/*
Copyright (c) 2002 JSON.org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the 'Software'), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

The Software shall be used for Good, not Evil.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/*
 * A JSONObject is an unordered collection of name/value pairs. Its
 * external form is a string wrapped in curly braces with colons between the
 * names and values, and commas between the values and names. The internal form
 * is an object having <code>get</code> and <code>opt</code> methods for
 * accessing the values by name, and <code>put</code> methods for adding or
 * replacing values by name. The values can be any of these types:
 * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>,
 * <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code>
 * object. A JSONObject constructor can be used to convert an external form
 * JSON text into an internal form whose values can be retrieved with the
 * <code>get</code> and <code>opt</code> methods, or to convert values into a
 * JSON text using the <code>put</code> and <code>toString</code> methods.
 * A <code>get</code> method returns a value if one can be found, and throws an
 * exception if one cannot be found. An <code>opt</code> method returns a
 * default value instead of throwing an exception, and so is useful for
 * obtaining optional values.
 * <p>
 * The generic <code>get()</code> and <code>opt()</code> methods return an
 * object, which you can cast or query for type. There are also typed
 * <code>get</code> and <code>opt</code> methods that do type checking and type
 * coersion for you.
 * <p>
 * The <code>put</code> methods adds values to an object. For example, <pre>
 *     myString = new JSONObject().put('JSON', "Hello, World!").toString();</pre>
 * produces the string <code>{"JSON": "Hello, World"}</code>.
 * <p>
 * The texts produced by the <code>toString</code> methods strictly conform to
 * the JSON sysntax rules.
 * The constructors are more forgiving in the texts they will accept:
 * <ul>
 * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
 *     before the closing brace.</li>
 * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
 *     quote)</small>.</li>
 * <li>Strings do not need to be quoted at all if they do not begin with a quote
 *     or single quote, and if they do not contain leading or trailing spaces,
 *     and if they do not contain any of these characters:
 *     <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers
 *     and if they are not the reserved words <code>true</code>,
 *     <code>false</code>, or <code>null</code>.</li>
 * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as
 *     by <code>:</code>.</li>
 * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as
 *     well as by <code>,</code> <small>(comma)</small>.</li>
 * <li>Numbers may have the <code>0-</code> <small>(octal)</small> or
 *     <code>0x-</code> <small>(hex)</small> prefix.</li>
 * <li>Comments written in the slashshlash, slashstar, and hash conventions
 *     will be ignored.</li>
 * </ul>
 * @author JSON.org
 * @version 3
 */ 
public class JSONObject {
        
    private static Map<String, Map<String,Schema.Sobjectfield>> sObjectFieldCache = new Map<String, Map<String,Schema.Sobjectfield>>();
    
    public static Map<String,Schema.Sobjectfield> getSObjectFields(Schema.SObjectType sObjectType) {
        String sObjectTypeAsString = String.valueOf(sObjectType);
        // If it's not already in the cache, add the sObject's field map to the cache
        if (!sObjectFieldCache.containsKey(sObjectTypeAsString)) {
           sObjectFieldCache.put(sObjectTypeAsString, sObjectType.getDescribe().fields.getMap());
        }
        return sObjectFieldCache.get(sObjectTypeAsString);
    }
    
    /* **
     * The map where the JSONObject's properties are kept.
     */
    private Map<String,Value> data = new Map<String,Value>();

    /* **
     * Construct an empty JSONObject.
     */
    public JSONObject() { }

    /*
     * Construct a JSONObject from a JSONTokener.
     * @param x A JSONTokener object containing the source string.
     * @ If there is a syntax error in the source string.
     */
    public JSONObject(JSONTokener x) {
        this();
        string c;
        String key;

        if (x.nextClean() != '{') {
            throw new JSONException('A JSONObject text must begin with {');
        }
        for (;;) {
            c = x.nextClean();
            if (c == null) {
                throw new JSONException('A JSONObject text must end with }');
            } else if (c == '}') {
                return;
            } else {
                x.back();
                key = (String)x.nextValue().strVal;
            }
    
            /*
             * The key is followed by ':'. We will also tolerate '=' or '=>'.
             */
            c = x.nextClean();
            if (c == '"') {
                c = x.nextClean();
            }
            
            if (c == '=') {
                if (x.next() != '>') {
                    x.back();
                }
            } else if (c != ':') {
                throw new JSONException('Expected a : after a key');
            }
            
            putOpt(key, x.nextValue()); // load next value into data map 
            
            /*
             * Pairs are separated by ','. We will also tolerate ';'.
             */
            string nc = x.nextClean();
            if (nc == ';' || nc ==  ',') {
                if (x.nextClean() == '}') {
                    return;
                }
                x.back();
            } else if (nc == '}') {
                return;
            } else {
                throw new JSONException('Expected a , or }');
            }
        }
    }
    
    public JSONObject (String source) {
        this(new JSONTokener(source));
    }
    
    /* **
     * Construct a JSONObject from a source JSON text string.
     * This is the most commonly used JSONObject constructor.
     * @param source    A string beginning
     *  with <code>{</code>&nbsp;<small>(left brace)</small> and ending
     *  with <code>}</code>&nbsp;<small>(right brace)</small>.
     * @exception JSONException If there is a syntax error in the source string.
     */
    public static JSONObject instance(String source) {
        return new JSONObject(new JSONTokener(source));
    }
    
    public value getValue(string key) {
        return data.get(key);
    }

    /* **
     * Get the value object associated with a key.
     *
     * @param key   A key string.
     * @return      The object associated with the key.
     * @throws   JSONException if the key is not found.
     */
    public object get(string key) {
        value ret = data.get(key);
        if (ret == null) {
            throw new JSONException('JSONObject['+ key +'] not found.');
        }
        if (ret.boolVal != null) return ret.boolVal;
        if (ret.intVal != null) return ret.intVal;
        if (ret.datetimeVal != null) return ret.datetimeVal;
        if (ret.dateVal != null) return ret.dateVal;
        if (ret.doubleVal != null) return ret.doubleVal;
        if (ret.strVal != null) return ret.strVal;
        if (ret.jsonObjVal != null) return ret.jsonObjVal;
        // Can't return List or Map because they are not of type Object

        return null;
    }

    /* **
     * Get the boolean value associated with a key.
     *
     * @param key   A key string.
     * @return      The truth.
     * @throws JSONException if the value is not a Boolean or the String 'true' or 'false'.
     */
    public boolean getBoolean(String key) {
        Object o = this.get(key);
        if (o == null) {
            throw new JSONException('JSONObject['+ key +'] is not a Boolean.');
        }
        return (boolean) o;
    }

    /* **
     * Get the string associated with a key.
     *
     * @param key   A key string.
     * @return      A string which is the value.
     * @throws   JSONException if the key is not found.
     */
    public String getString(String key) {
        return this.data.get(key).strVal;
    }
    
    /* **
     * Get the string associated with a key.
     *
     * @param key   A key string.
     * @return      A string which is the value.
     * @throws   JSONException if the key is not found.
     */
    public JSONObject getJsonObject(String key) {
        return this.data.get(key).jsonObjVal;
    }

    /* **
     * Determine if the JSONObject contains a specific key.
     * @param key   A key string.
     * @return      true if the key exists in the JSONObject.
     */
    public boolean has(String key) {
        return this.data.containsKey(key);
    }
    
    /* **
     * Get an enumeration of the keys of the JSONObject.
     *
     * @return A set of the keys.
     */
    public set<string> keys() {
        return this.data.keySet();
    }

    /* **
     * Get the number of keys stored in the JSONObject.
     *
     * @return The number of keys in the JSONObject.
     */
    public integer length() {
        return this.data.keySet().size();
    }

    /* **
     * Get an optional value associated with a key.
     * @param key   A key string.
     * @return      An object which is the value, or null if there is no value.
     */
    public Object opt(String key) {
        return key == null ? null : this.get(key);
    }

    /* **
     * Get an optional boolean associated with a key.
     * It returns false if there is no such key, or if the value is not
     * true or the String 'true'.
     *
     * @param key   A key string.
     * @return      The truth.
     */
    public boolean optBoolean(String key) {
        return optBoolean(key, false);
    }

    /* **
     * Get an optional boolean associated with a key.
     * It returns the defaultValue if there is no such key, or if it is not
     * a Boolean or the String 'true' or 'false' (case insensitive).
     *
     * @param key              A key string.
     * @param defaultValue     The default.
     * @return      The truth.
     */
    public boolean optBoolean(String key, boolean defaultValue) {
        try {
            return getBoolean(key);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /* **
     * Put a key/value pair in the JSONObject, but only if the
     * key and the value are both non-null.
     * @param key   A key string.
     * @param value An object which is the value. It should be of one of these
     *  types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String,
     *  or the JSONObject.NULL object.
     * @return this.
     * @ If the value is a non-finite number.
     */
    public JSONObject putOpt(String key, value value) {
        if (key != null && value != null) {
            data.put(key, value);
        }
        return this;
    }
    
    /* **
     * valueToString 
     * Make a JSON text of an Object value. The method is required to produce a strictly
     * conforming text. If the object does not contain a toJSONString
     * method (which is the most common case), then a text will be
     * produced by other means.  the
     * value's toString method will be called, and the result will be quoted.
     *
     * <p>
     * Warning: This method assumes that the data structure is acyclical.
     * @param value The value to be serialized.
     * @return a printable, displayable, transmittable
     *  representation of the object, beginning
     *  with <code>{</code>&nbsp;<small>(left brace)</small> and ending
     *  with <code>}</code>&nbsp;<small>(right brace)</small>.
     * @ If the value is or contains an invalid number.
     */
    public String valueToString() {
        String ret = ' ';
        for (String key : this.keys()) {
            ret += '"'+ key +'": '+ this.getvalue(key).valueToString() +',';
        }
        return '{'+ ret.substring(0, ret.length() - 1) +'}';
    }
    
    
    public class JSONException extends Exception {}
 
    /* 
     * apex object to store one of several different data types
     * see : http://www.json.org/   
     */ 
    public class Value {
        /*
         * Different types that
          can be wrapped as a JSON value
         */
        public Boolean boolVal;
        public Integer intVal;
        public DateTime datetimeVal;
        public Date dateVal;
        public Double doubleVal;
        public String strVal;
        public JSONObject jsonObjVal;
        public List<Value> listVal;
        public List<String> stringList;
        public Map<String,String> mapVal;
        public Map<String,Integer> intMapVal;
        public Map<String,JSONObject.value> jsonValMap;
        public Map<Id,SObject> sObjectMap;
        public Set<String> stringSet;
        
        /*
         * Constructors for the different types accepted
         */
        public value() { /* null value */ }
        public value(Boolean bb) { 
            boolVal = bb;
        }
        public value(Integer ii) {
            intVal = ii;
        }
        public value(DateTime ii) {
            datetimeVal = ii;
        }
        public value(Date ii) {
            dateVal = ii;
        }
        public value(Decimal ii) {
            doubleVal = ii;
        }
        public value(String st) {
            strVal = st;
        }
        public value(JSONObject oo) {
            jsonObjVal = oo;
        }
        public value(List<value> vary) {
            listVal = vary;
        }
        public value(List<string> vary) {
            stringList = vary;
        }
        public value(Map<String,String> m) {
            mapVal = m;
        }
        public value(Map<String,Integer> m) {
            intMapVal = m;
        }
        public value(Map<Id,SObject> m) {
            sObjectMap = m;
        }
        public value(Map<String,JSONObject.Value> m) {
            jsonValMap = m;
        }
        public value(Set<String> ss) {
            stringSet = ss;
        }
        
        /* 
         * Value to string is used to format output as valid JSON
         */
        public string valueToString() {
            // Value is an sObject map, create JSON object representing map key => value pairs
            if (stringSet != null) {
                return stringSetToString(stringSet);
            }
            // Value is an JSONObject.Value map, create JSON object representing map key => value pairs
            if (jsonValMap != null) {
                return jsonValMapToString(jsonValMap);
            }
            // Value is an sObject map, create JSON object representing map key => value pairs
            if (sObjectMap != null) {
                return sObjectMapToString(sObjectMap);
            }
            if (intMapVal != null) {
                return intMapToString(intMapVal);
            }
            // Value is a map, create JSON object representing map key => value pairs
            if (mapVal != null) {
                return stringMapToString(mapVal);
            }
            // boolean - return as unquoted true or false
            if (boolVal != null) {
                return (boolVal ? 'true' : 'false');
            }
            // String - wrap in double quotes
            if (strVal != null) {
                return encodeString(strVal);
            }
            // Integer - return as unquoted string
            if (intVal != null) {
                return String.valueof(intVal);
            }
            // Date - return as unquoted string
            if (dateVal != null) {
                return String.valueof(dateVal);
            }
            // Datetime - return as unquoted string
            if (datetimeVal != null) {
                return String.valueof(datetimeVal);
            }
            // Decimal - return as unquoted string
            if (doubleVal != null) {
                return String.valueof(doubleVal);
            }
            // Value is an sObject map, create JSON object representing map key => value pairs
            if (stringList != null) {
                String ret = ' ';
                for (String v : stringList) {
                    ret += encodeString(v) +',';
                }
                return '['+ ret.substring(0, ret.length() - 1) +']';
            }
            // List - return as JS array
            if (listVal != null) {
                String ret = ' ';
                for (value v : listVal) {
                    ret += v.valueToString() +',';
                }
                return '['+ ret.substring(0, ret.length() - 1) +']';
            }
            // JSONObject
            if (jsonObjVal != null) {
                return jsonObjVal.valueToString();
            }
            
            // Nothing set, value should be an empty string
            return '""';
        }
        
        public String stringSetToString(Set<String> stringSet) {
            String ret = ' ';
            for (String key : StringSet) {
                ret += encodeString(key) + ': true,';
            }
            // Wrap in curly braces to make a valid JSON object
            return '{'+ ret.substring(0, ret.length() - 1) +'}';
        }
        
        /*
         * Turn the map value into a JSON object of key => value pairs
         * @param mapVal Key value pairs to represent as a JSON object
         */
        public String jsonValMapToString(Map<String,JSONObject.Value> mapVal) {
            String ret = ' ';
            for (String key : mapVal.keySet()) {
                JSONObject.Value value = mapVal.get(key);
                if (value == null) {
                   ret += '"'+ key +'": "",';
                } else {
                   ret += '"'+ key +'": '+ value.valueToString() +',';
                }
            }
            // Wrap in curly braces to make a valid JSON object
            return '{'+ ret.substring(0, ret.length() - 1) +'}';
         }
        
        /*
         * Turn the map value into a JSON object of key => value pairs
         * @param mapVal Key value pairs to represent as a JSON object
         */
        public String sObjectMapToString(Map<Id,sObject> mapVal) {
            String ret = ' ';
            for (Id key : mapVal.keySet()) {
                SObject value = mapVal.get(key);
                if (value == null) {
                   ret += '"'+ key +'": "",';
                } else {
                   ret += '"'+ key +'": '+ stringMapToString(convertSObjectToStringMap(value)) +',';
                }
            }
            // Wrap in curly braces to make a valid JSON object
            return '{'+ ret.substring(0, ret.length() - 1) +'}';
        }
        
        public Map<String,String> convertSObjectToStringMap(SObject sObj) {
            Map<String, String> resultMap = new Map<String,String>();
            // Iterate over all possible fields on the object to find the ones in the sObject passed in
            for (String fieldName : getSObjectFields(sObj.getSObjectType()).keySet()) {
                // Passed in object has the field
                try {
                    resultMap.put(fieldName, String.valueOf(sObj.get(fieldName)));
                } catch (System.SObjectException except) { /* field not found in sObj*/}
            }
            return resultMap;
        }
        
        /*
         * Turn the map value into a JSON object of key => value pairs
         * @param mapVal Key value pairs to represent as a JSON object
         */
        public String stringMapToString(Map<String,String> mapVal) {
            String ret = ' ';
            for (String key : mapVal.keySet()) {
                String value = mapVal.get(key);
                if (value == null) {
                   ret += '"'+ key +'": "",';
                } else {
                   ret += '"'+ key +'": '+ encodeString(value) +',';
                }
            }
            // Wrap in curly braces to make a valid JSON object
            return '{'+ ret.substring(0, ret.length() - 1) +'}';
        }
        
        /*
         * Turn the map value into a JSON object of key => value pairs
         * @param mapVal Key value pairs to represent as a JSON object
         */
        public String intMapToString(Map<String,Integer> mapVal) {
            String ret = ' ';
            for (String key : mapVal.keySet()) {
                Integer value = mapVal.get(key);
                if (value == null) {
                   ret += '"'+ key +'": "",';
                } else {
                   ret += '"'+ key +'": '+ value +',';
                }
            }
            // Wrap in curly braces to make a valid JSON object
            return '{'+ ret.substring(0, ret.length() - 1) +'}';
        }
        
        public string encodeString(String str) {
            return '"'+ str.replaceAll('\n', '\\\\n').replaceAll('"', '\\\\"') +'"';
        }
    }

    /*
     * A JSONTokener takes a source string and extracts characters and tokens from
     * it. It is used by the JSONObject and JSONArray constructors to parse
     * JSON source strings.
     * @author JSON.org
     * @version 3
     */
    public class JSONTokener {
        private integer index;
        private string reader;
        private string lastChar;
        private boolean useLastChar;
    
        /*
         * Construct a JSONTokener from a string.
         *
         * @param str A source string.
         */
        public JSONTokener(String str) {
            this.useLastChar = false;
            this.index = 0;
            this.reader = str;
        }
    
        /*
         * Back up one character. This provides a sort of lookahead capability,
         * so that you can test for a digit or letter before attempting to parse
         * the next number or identifier.
         */
        public void back() {
            if (useLastChar || index <= 0) {
                throw new JSONException('Stepping back two steps is not supported');
            }
            index -= 1;
            useLastChar = true;
        }
    
        /*
         * Determine if the source string still contains characters that next()
         * can consume.
         * @return true if not yet at the end of the source.
         */
        public boolean more() {
            String nextChar = next();
            if (nextChar == null) {
                return false;
            }
            back();
            return true;
        }
    
        public String read(string reader) {
            if (index + 1 > reader.length()) {
                return null;
            }
            return reader.substring(index, index + 1);
        }
        
        /*
         * Get the next character in the source string.
         *
         * @return The next character, or 0 if past the end of the source string.
         */
        public String next() {
            if (this.useLastChar) {
                this.useLastChar = false;
                if (this.lastChar != null) {
                    this.index += 1;
                }
                return this.lastChar;
            }
            string c;
            try {
                c = read(reader);
            } catch (exception exc) {
                throw new JSONException(exc);
            }
    
            if (c == null) { // End of stream
                this.lastChar = null;
                return null;
            }
            this.index += 1;
            this.lastChar = (String) c;
            return this.lastChar;
        }
    
        /*
         * Consume the next character, and check that it matches a specified
         * character.
         * @param c The character to match.
         * @return The character.
         * @throws JSONException if the character does not match.
         */
        public String next(String c) {
            String n = next();
            if (n != c) {
                throw new JSONException('Expected ' + c + ' and instead saw ' + n );
            }
            return n;
        }
    
        /*
         * Get the next n characters.
         *
         * @param n     The number of characters to take.
         * @return      A string of n characters.
         * @throws JSONException
         *   Substring bounds error if there are not
         *   n characters remaining in the source string.
         */
         public String next(Integer n) {
             if (n == 0) {
                 return '';
             }
    
             String buffer = '';
             integer pos = 0;
    
             if (this.useLastChar) {
                 this.useLastChar = false;
                 buffer = this.lastChar;
                 pos = 1;
             }
    
             try {
                 buffer = buffer + reader.substring(index, index+n);
                 pos += n;
             } catch (Exception exc) {
                 throw new JSONException(exc.getMessage());
             }
             this.index += pos;
    
             if (pos < n) {
                 throw new JSONException('Substring bounds error');
             }
    
             this.lastChar = buffer.substring(buffer.length() - 1);
             return buffer;
         }
    
        /*
         * Get the next char in the string, skipping whitespace
         * and comments (slashslash, slashstar, and hash).
         * @throws JSONException
         * @return  A character, or 0 if there are no more characters.
         */
        public string nextClean() {
            for (;;) {
                string c = next();
                // Look for // or /* comments
                if (c == '/') {
                    string n = next();
                    // Ignore // comments
                    if ( n == '/' ) {
                        do {
                            c = next();
                        } while (c != '\n' && c != '\r' && c != null);
                    }
                    // Ignore /* comments
                    else if (n ==  '*') {
                        for (;;) {
                            c = next();
                            if (c == null) {
                                throw new JSONException('Unclosed comment');
                            }
                            if (c == '*') {
                                if (next() == '/') {
                                    break;
                                }
                                back();
                            }
                        }
                    } else {
                        back();
                        return '/';
                    }
                // Ignore # comments
                } else if (c == '#') {
                    do {
                        c = next();
                    } while (c != '\n' && c != '\r' && c != null);
                }
                
                if (c == null || c > ' ') {
                    return c;
                }
            }
            return null;
        }
    
        /*
         * Return the characters up to the next close quote character.
         * Backslash processing is done. The formal JSON format does not
         * allow strings in single quotes, but an implementation is allowed to
         * accept them.
         * @param quote The quoting character, either
         *      <code>"</code>&nbsp;<small>(double quote)</small> or
         *      <code>'</code>&nbsp;<small>(single quote)</small>.
         * @return      A String.
         * @throws JSONException Unterminated string.
         */
        public String nextString(String quote) {
            String c;
            String sb = '';
            for (;;) {
                c = next();
                if ( c == null || c == '\n' ||  c == '\r') {
                    throw new JSONException('Unterminated string');
                }
        
                if (c == '\\') {
                    c = next();
                }
                else if (c == quote) {
                    return sb;
                }
                sb = sb + c;
                
            }
            return '';
        }
    
        /**
         * Get the text up but not including the specified character or the
         * end of line, whichever comes first.
         * @param  d A delimiter character.
         * @return   A string.
         */
        public String nextTo(string d) {
            String sb = '';
            for (;;) {
                String c = next();
                if (c == d || c == null || c == '\n' || c == '\r') {
                    if (c != null) {
                        back();
                    }
                    return sb.trim();
                }
                sb = sb + c;
            }
            return '';
        }
    
        /*
         * Get the next value. The value can be a Boolean, Double, Integer,
         * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
         * @throws JSONException If syntax error.
         *
         * @return An object.
         */
        public Value nextValue() {
            String c = nextClean();
            String s;
    
            if (c == '"' || c == '\'') {
                return new Value(nextString(c));
            }
            
            if (c == '{') {
                back();
                return new Value(new JSONObject(this));
            }
            
            if (c == '[' || c ==  '(') {
                back();
                return new Value(JSONArray(this));
            }
    
            /*
             * Handle unquoted text. This could be the values true, false, or
             * null, or it can be a number. An implementation (such as this one)
             * is allowed to also accept non-standard forms.
             *
             * Accumulate characters until we reach the end of the text or a
             * formatting character.
             */
            String sb = '';
            String b = c;
            while (c >= ' ' && '),:]}/\\\"[{;=#'.indexOf(c) < 0) {
                sb = sb + c;
                c = next();
                
            }
            back();
    
            /*
             * If it is true, false, or null, return the proper value.
             */
            s = sb.trim();
            if (s.equals('')) {
                throw new JSONException('Missing value');
            }
            if (s.equalsIgnoreCase('true')) {
                return new Value(true);
            }
            if (s.equalsIgnoreCase('false')) {
                return new Value( false);
            }
            if (s.equalsIgnoreCase('null')) {
                return new Value();
            }
    
            /*
             * If it might be a number, try converting it. We support the 0- and 0x-
             * conventions. If a number cannot be produced, then the value will just
             * be a string. Note that the 0-, 0x-, plus, and implied string
             * conventions are non-standard. A JSON parser is free to accept
             * non-JSON forms as long as it accepts all correct JSON forms.
             */
            if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') {
                if (b == '0') {
                    if (s.length() > 2 && (s.substring(1,2) == 'x' || s.substring(1,2) == 'X')) {
                        try {
                            return new Value(Integer.valueof(s.substring(2)));
                        } catch (Exception e) {
                            /* Ignore the error */
                        }
                    } else {
                        try {
                            return new Value(Integer.valueof(s));
                        } catch (Exception e) {
                            /* Ignore the error */
                        }
                    }
                }
                try {
                    return new Value(Integer.valueof(s));
                } catch (Exception e) {
                    /* Ignore the error */
                }
            }
            return new Value(s);
        }
    
        /**
         * Skip characters until the next character is the requested character.
         * If the requested character is not found, no characters are skipped.
         * @param to A character to skip to.
         * @return The requested character, or zero if the requested character
         * is not found.
         */
        public String skipTo(String to) {
            String c;
            try {
                Integer startIndex = this.index;
                do {
                    c = next();
                    if (c == null) {
                        this.index = startIndex;
                        return c;
                    }
                } while (c != to);
            } catch (Exception exc) {
                throw new JSONException(exc);
            }
    
            back();
            return c;
        }
    }

   /*
     * Construct a JSONArray from a JSONTokener.
     * @param x A JSONTokener
     * @throws JSONException If there is a syntax error.
     */
    public static List<Value> JSONArray(JSONTokener x) {
        List<Value> myArrayList = new List<Value>();
        String c = x.nextClean();
        String q;
        if (c == '[') {
            q = ']';
        } else if (c == '(') {
            q = ')';
        } else {
            throw new JSONException('A JSONArray text must start with [');
        }
        if (x.nextClean() == ']') {
            return myArrayList;
        }
        
        x.back();
        for (;;) {
            if (x.nextClean() == ',') {
                x.back();
                myArrayList.add(null);
            } else {
                x.back();
                myArrayList.add(x.nextValue());
            }
            c = x.nextClean();
            if (c == ';' || c == ',') {
                if (x.nextClean() == ']') {
                    return myArrayList;
                }
                x.back();
               
            } else if (c == ']' || c == ')') {
                if (q != c) {
                    throw new JSONException('Expected a >'+ q +'<');
                }
                return myArrayList;
            } else {
                throw new JSONException('Expected a , or ]');
            }
        }
        return null;// not reached
    }



    /* **************************
     * TEST Methods to achieve required code coverage
     *   many of these could be improved by using assert() instead of debug()
     */
     
    public static testmethod void testEncodeString_EscapesDoubleQuotes() {
        String initialString = 'my name is not "mud"!';
        String expectedString = '"my name is not \\"mud\\"!"';
        
        JSONObject.Value obj = new JSONObject.Value();
        System.assertEquals(expectedString, obj.encodeString(initialString));
    }
     
    public static testmethod void testEncodeString_EscapesNewLines() {
        String initialString = 'my name is not \n mud!';
        String expectedString = '"my name is not \\n mud!"';
        
        JSONObject.Value obj = new JSONObject.Value();
        System.assertEquals(expectedString, obj.encodeString(initialString));
    }
        
    public static testmethod void testConvertSObjectToStringMap() {
        Map<Id,Contact> contactMap = new Map<Id,Contact>([select id, firstName, email from contact limit 2]);
        JSONObject.Value jsonObj = new JSONObject.value(contactMap);
        String jsonStr = jsonObj.valueToString();
        System.debug(jsonStr);
    }
    
/*    public static testmethod void testParsingThenEncodingPreservesOriginal() {
        String jsonString = '{ "FollowedRecords": { "003A0000007NykMIAS": true,"001A00000085GRrIAM": true},"RecordList": [ "001A00000085GRrIAM","001A0000007uJkHIAU","00QA00000062k48MAA","003A0000007L4uEIAS","003A0000007L4uJIAS","003A0000007NykMIAS"],"RecordMap": { "003A0000007L4uJIAS": { "Name": "Avi Green","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/4/2010 5:06 PM","PostBody": "Greater!"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 10:38 PM","PostBody": "great!"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 10:29 PM","PostBody": "Test message 2"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 9:46 PM","PostBody": "true"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 9:33 PM","PostBody": "test"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 9:32 PM","PostBody": "test"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 9:31 PM","PostBody": "test"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 9:30 PM","PostBody": "test"}],"PostDate": "4/4/2010 5:06 PM","Following": false,"Id": "003A0000007L4uJIAS","Company": "United Oil & Gas Corp."},"00QA00000062k48MAA": { "Name": "Andy Young","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 11:01 PM","PostBody": "Lead message"}],"PostDate": "4/3/2010 11:01 PM","Following": false,"Id": "00QA00000062k48MAA","Company": "Dickenson plc"},"001A0000007uJkHIAU": { "Name": "United Oil & Gas Corp.","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/3/2010 10:52 PM","PostBody": "Test account note"}],"PostDate": "4/3/2010 10:52 PM","Following": false,"Id": "001A0000007uJkHIAU","Company": "United Oil & Gas Corp."},"003A0000007NykMIAS": { "Name": "Alex Korn","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/4/2010 5:24 PM","PostBody": "This is a test annotation with a new line\\n Parent.Id, Parent.firstName, Parent.lastName, Parent.email, Parent.Account.name,\\n (SELECT Id, FieldName, OldValue, NewValue FROM FeedTrackedChanges ORDER BY Id DESC)"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/4/2010 4:28 PM","PostBody": "Contact reassigned from \\"Ryan Ausanka-Crues\\" to \\"Alex Korn\\""},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/1/2010 7:37 PM","PostBody": "This is a test of the emergency broadcast system"},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "3/30/2010 2:51 PM","PostBody": "Record created by Ryan Ausanka-Crues"}],"PostDate": "4/4/2010 5:24 PM","Following": true,"Id": "003A0000007NykMIAS","Company": "Foo bar"},"003A0000007L4uEIAS": { "Name": "Arthur Song","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "4/4/2010 2:52 PM","PostBody": "Arthur\'s Message"}],"PostDate": "4/4/2010 2:52 PM","Following": false,"Id": "003A0000007L4uEIAS","Company": "United Oil & Gas Corp."},"001A00000085GRrIAM": { "Name": "Foo bar","Posts": [ { "PostCreator": "Ryan Ausanka-Crues","PostDate": "3/30/2010 11:53 AM","PostBody": "The funnel is amazing! Great work, guys."},{ "PostCreator": "Ryan Ausanka-Crues","PostDate": "3/26/2010 5:26 PM","PostBody": "Record created by Ryan Ausanka-Crues"}],"PostDate": "3/30/2010 11:53 AM","Following": true,"Id": "001A00000085GRrIAM","Company": "Foo bar"}}}';
        System.debug(LoggingLevel.Warn, jsonString);
        JSONObject obj = new JSONObject(jsonString);
        System.assertEquals(jsonString, obj.valueToString());
    }*/
        
    public static testmethod void test_tokener_nl() {
        //add a new line by inserting /n 
        JsonObject.JsonTokener tkr = new JsonTokener('quoted string foo"');
        system.debug('next string is >'+ tkr.nextString('"'));
    }
    
    public static testmethod void test_tokener() {
        JsonObject.JsonTokener tkr = new JsonTokener('//ff \n{}');
        tkr.nextClean();
        tkr = new JsonTokener('/*fff*/\n{}');
        tkr.nextClean();
        tkr = new JsonTokener('#ff*\n{}');
        tkr.nextClean();
        tkr = new JsonTokener('/**ff \n{}');
        try {
            tkr.nextClean();
        } catch(exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'Unclosed comment');
        }
    }
    
    public static  testmethod void test_parsing() {
        JSONObject jj; JsonObject.JsonTokener tkr;
        string simple = '{"translatedText":  "Ciao mondo"  }';
        string bool_simple = '{"responseBoolean"  :   true }';
        string nsimple = '{"responseData"  :   {"translatedText":  "Ciao mondo"  }, "responseDetails": null, "responseStatus": 200 }';
    
        // three methods of constructing an object
        system.debug(jsonobject.instance(bool_simple));
        system.debug(new jsonobject(bool_simple));
        system.debug(new JSONObject(new JsonTokener(simple)));
        
        tkr = new JsonTokener(nsimple);
        system.debug(tkr.more());
        system.debug(tkr.next(1));
        system.assert(tkr.next(0) == '');
        
        try {
            tkr.next(10000);
        } catch(Exception ex) {
            system.debug(' expected >'+ex.getMessage() + '<');
            system.assert(ex.getMessage() == 'Ending position out of bounds: 10002');
        }
        
        system.debug(tkr.next('r'));
        
        try {
            tkr.next('z');
        } catch(Exception ex) {
            System.debug(' expected >'+ ex.getMessage() + '<');
            System.assert(ex.getMessage() == 'Expected z and instead saw e');
        }
            
        system.debug(tkr.nextTo('e'));
        system.debug(tkr.skipTo('p'));
        system.debug(tkr.skipTo('z'));
            
        tkr = new JSONTokener(nsimple);
        jj = new JSONObject(tkr);
         
        system.debug(jj.data);
        system.debug(jj.data.keySet());
        
        system.debug('response status '+ jj.data.get('responseStatus'));
        system.debug('response status '+ jj.get('responseStatus'));
        system.debug('response details '+ jj.get('responseDetails'));
        
        system.debug(jj.getString('responseDetails'));
        system.assert(jj.getString('responseDetails') == null ,'expected null ');
        
        system.debug('value '+ jj.getValue('responseData'));
        
        value v = jj.data.get('responseData');
        system.debug(jj.getString('responseDetails'));
        system.debug('response data is '+ v.jsonObjVal);
         
        system.debug(jj.data.keySet());
        
        nsimple = '{"responseString"  :   "foo" }';
        tkr = new JSONTokener(nsimple);
        jj = new JSONObject(tkr);
        system.debug(jj.get('responseString'));
         
        nsimple = '{"responseBoolean"  :   true }';
        tkr = new JSONTokener(nsimple);
        jj = new JSONObject(tkr);
        system.debug(jj.getValue('responseBoolean'));
        system.debug(jj.optBoolean('responseBoolean'));
        
        try {
            system.debug(new JSONObject(new JSONTokener('sdfsdf')));
        } catch(Exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'A JSONObject text must begin with {');
        }
        
        try {
            system.debug(new JSONObject(new JSONTokener('{"sdfsdf": true')));
        } catch(Exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'Expected a , or }');
        }
        
        try {
            system.debug(new JSONObject(new JSONTokener('{"sdfsdf')));
        } catch(Exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'Unterminated string');
        }
    
        try {
            system.debug(new JSONObject(new JSONTokener('{"sdfsdf": 0x009.9 }')));
        } catch(Exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'Invalid integer: 009.9');
        }
    
        system.assert(new JSONObject(new JSONTokener('{"sdfsdf": 009 }')).getValue('sdfsdf').intVal == 9);
        
        system.debug(new JSONObject(new JSONTokener('{"foo": 009 }')).get('foo') == 9);
        
        // array testing
        system.debug(JSONObject.jsonarray(new JSONTokener('[1,2,3]')));
        system.debug(JSONObject.jsonarray(new JSONTokener('[1,2,3,]')));
        system.debug(JSONObject.jsonarray(new JSONTokener('[]')));
        system.debug(JSONObject.jsonarray(new JSONTokener('(1,2,3 )')));
        
        try {
            system.debug(JSONObject.jsonarray(new JSONTokener('(1,2,3 ]')));
        } catch(Exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'Expected a >)<');
        }
        
        try {
            system.debug(JSONObject.jsonarray(new JSONTokener('1,2,3 ]')));
        } catch(exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'A JSONArray text must start with [');
        }
    
        try {
            system.debug(jj.get('notfound'));
        } catch(exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'JSONObject[notfound] not found.');
        }
        
        system.assert(jj.keys() != null);
        system.assert(jj.length() > 0);
        system.assert(jj.opt('responseBoolean') != null);
        system.assert(jj.has('responseBoolean'));
        try {
            system.debug(jj.getBoolean('notfound'));
        } catch(exception ex) {
            system.debug(' expected >'+ ex.getMessage() +'<');
            system.assert(ex.getMessage() == 'JSONObject[notfound] not found.');
        }
    }
    
    public static testmethod void testValue_ByBooleanTrue() {
        JSONObject.Value obj = new JSONObject.Value(true);
        System.assertEquals('true', obj.valueToString());
    }
    
    public static testmethod void testValue_ByBooleanFalse() {
        JSONObject.Value obj = new JSONObject.Value(false);
        System.assertEquals('false', obj.valueToString());
    }
    
    public static testmethod void testValue_ByInteger() {
        JSONObject.Value obj = new JSONObject.Value(10);
        System.assertEquals('10', obj.valueToString());
    }
    
    public static testmethod void testValue_ByDateTime() {
        JSONObject.Value obj = new JSONObject.Value(DateTime.now());
        System.assert(Pattern.matches('[\\d]{4}-[\\d]{2}-[\\d]{2}\\s[\\d]{2}:[\\d]{2}:[\\d]{2}', obj.valueToString()));
    }
    
    public static testmethod void testValue_ByDate() {
        JSONObject.Value obj = new JSONObject.Value(Date.today());
        System.assert(Pattern.matches('[\\d]{4}-[\\d]{2}-[\\d]{2}', obj.valueToString()));
    }
    
    public static testmethod void testValue_ByDecimal() {
        JSONObject.Value obj = new JSONObject.Value(123.123);
        System.assertEquals('123.123', obj.valueToString());
    }
    
    public static testmethod void testValue_ByString() {
        JSONObject.Value obj = new JSONObject.Value('123123');
        System.assertEquals('"123123"', obj.valueToString());
    }
    
    public static testmethod void testValue_WithEmptyListReturnsEmptyList() {
        JSONObject.Value obj = new JSONObject.Value(new List<Value>{});
        System.assertEquals('[]', obj.valueToString());
    }
    
    public static testmethod void testValue_WithList() {
        JSONObject.Value obj = new JSONObject.Value(new List<Value>{new JSONObject.Value('123'),new JSONObject.Value('456')});
        System.assertEquals('[ "123","456"]', obj.valueToString());
    }
    
    public static testmethod void testValue_WithEmptyMapReturnsEmptyObject() {
        JSONObject.Value obj = new JSONObject.Value(new Map<String,String>{});
        System.assertEquals('{}', obj.valueToString());
    }
    
    public static testmethod void testValue_WithMap() {
        JSONObject.Value obj = new JSONObject.Value(new Map<String,String>{'123' => 'abc', '456 789' => 'cde'});
        System.assertEquals('{ "456 789": "cde","123": "abc"}', obj.valueToString());
    }
    
    public static testmethod void testValue_WithMapWithNullValueSetsAsEmptyString() {
        JSONObject.Value obj = new JSONObject.Value(new Map<String,String>{'123'=>null});
        System.assertEquals('{ "123": ""}', obj.valueToString());
    }
    
    public static testmethod void testValue_WithNullReturnsEmptyString() {
        JSONObject.Value obj = new JSONObject.Value();
        System.assertEquals('""', obj.valueToString());
    }
}